<!DOCTYPE html>
<html>
<head>
  <title>type_test</title>
  <script src="test_bootstrap.js"></script>
  <script type="text/javascript">
    goog.require('bot.action');
    goog.require('bot.events');
    goog.require('bot.Keyboard.Keys');
    goog.require('bot.userAgent');
    goog.require('goog.events');
    goog.require('goog.events.EventType');
    goog.require('goog.testing.AsyncTestCase');
    goog.require('goog.testing.jsunit');
    goog.require('goog.userAgent');
  </script>
  <script type="text/javascript">
    var asyncTestCase = null;

    // In Opera 11.5+, synthetic keypresses currently exhibit flaky failures:
    // http://code.google.com/p/selenium/issues/detail?id=3068
    // Most typing test methods below are skipped for these versions.
    var BROKEN_KEYPRESS = goog.userAgent.OPERA &&
        bot.userAgent.isEngineVersion(11.5);

    var INPUT_IDS = ['textbox', 'password', 'email', 'search', 'textarea'];

    var ALT = bot.Keyboard.Keys.ALT;
    var BACKSPACE = bot.Keyboard.Keys.BACKSPACE;
    var CONTROL = bot.Keyboard.Keys.CONTROL;
    var DELETE = bot.Keyboard.Keys.DELETE;
    var ENTER = bot.Keyboard.Keys.ENTER;
    var LEFT = bot.Keyboard.Keys.LEFT;
    var META = bot.Keyboard.Keys.META;
    var RIGHT = bot.Keyboard.Keys.RIGHT;
    var SHIFT = bot.Keyboard.Keys.SHIFT;

    function setUpPage() {
      for (var i = 0; i < INPUT_IDS.length; i++) {
        var input = document.getElementById(INPUT_IDS[i]);
        goog.events.listen(input, 'input', incrInputCount);
        goog.events.listen(input, 'propertychange', incrInputCount);
        goog.events.listen(input, 'textInput', incrTextInputCount);
      }
    }

    /**
     * Increments the value of the input count box by one.
     */
    function incrInputCount() {
      var inputCountBox = document.getElementById('inputCount');
      inputCountBox.value = parseInt(inputCountBox.value) + 1;
    }

    /**
     * Increments the value of the text input count box by one.
     */
    function incrTextInputCount() {
      var textInputCountBox = document.getElementById('textInputCount');
      textInputCountBox.value = parseInt(textInputCountBox.value) + 1;
    }

    /*
     * Types a value into textbox, and makes sure that textbox and textbox_count
     * have the correct values afterwards.  textbox_count should display the
     * number of times textbox's oninput (non IE) or onpropertychange (IE) event
     * fired.
     * @param {string|bot.Keyboard.Key|Array.<string, bot.Keyboard.Key>}
     *   input Value to type into textbox.
     * @param {string=} opt_finalValue The final value that should appear in
     *   the input.  Defaults to input.
     * @param {number=} opt_inputCount The number of input events that should
     *   have fired. Defaults to the length of the final value.
     * @param {number=} opt_textInputCount The number of textInput events that
     *   should have fired in WebKit. Defaults to the input count.
     * @param {!Array.<string>} opt_inputIds The ids of the inputs that the
     *   typing test should run on. Defaults to all of them.
     * @param {string} opt_initValue Initial value of the input. Defaults to
     *   the empty string.
     */
    function runTypingTest(input, opt_finalValue, opt_inputCount,
                           opt_textInputCount, opt_inputIds, opt_initValue) {
      if (BROKEN_KEYPRESS) {
        return;
      }
      var inputIds = opt_inputIds ? opt_inputIds : INPUT_IDS;
      var inputCountBox = document.getElementById('inputCount');
      var textInputCountBox = document.getElementById('textInputCount');
      var finalValue = goog.isDef(opt_finalValue) ? opt_finalValue : input;
      var inputCount =
          goog.isDef(opt_inputCount) ? opt_inputCount : finalValue.length;
      var textInputCount =
          goog.isDef(opt_textInputCount) ? opt_textInputCount : inputCount;
      var initValue = opt_initValue || '';

      for (var i = 0; i < inputIds.length; i++) {
        var inputBox = document.getElementById(inputIds[i]);
        inputBox.value = initValue;
        inputCountBox.value = '0';
        textInputCountBox.value = '0';
        bot.action.type.apply(null, [inputBox].concat(input));
        assertEquals(finalValue, inputBox.value);

        // On IE the onpropertychange event is not fired reliably when type is
        // called, so our tests are a little less strict when it comes to
        // checking IE.
        if (goog.userAgent.IE) {
          assertTrue(inputCount - 4 < parseInt(inputCountBox.value));
          assertTrue(inputCount >= parseInt(inputCountBox.value));
        } else {
          assertEquals(inputCount, parseInt(inputCountBox.value));
        }

        // Only WebKit fires textInput events.
        if (goog.userAgent.WEBKIT) {
          assertEquals(textInputCount, parseInt(textInputCountBox.value));
        } else {
          assertEquals(0, parseInt(textInputCountBox.value));
        }
      }
    }

    function testLowerCase() {
      runTypingTest('abcdefghijklmnopqrstuvwxyz');
    }

    function testUpperCase() {
      runTypingTest('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
    }

    function testNumbers() {
      runTypingTest('1234567890');
    }

    function testSymbols() {
      runTypingTest('`~!@#$%^&*()-_=+[{]}\\|;:\'"",<.>/?');
    }

    function testLeftRight() {
      runTypingTest(['aaaa', LEFT, LEFT, 'bb', RIGHT, 'c'], 'aabbaca');
    }

    function testLeftRightBounds() {
      runTypingTest([LEFT, LEFT, 'b',
                     LEFT, LEFT, 'a',
                     RIGHT, 'c',
                     RIGHT, RIGHT, RIGHT, 'e',
                     LEFT, 'd'],
                    'abcde');
    }

    function testNumPad() {
      runTypingTest([bot.Keyboard.Keys.NUM_ZERO,
                     bot.Keyboard.Keys.NUM_ONE,
                     bot.Keyboard.Keys.NUM_TWO,
                     bot.Keyboard.Keys.NUM_THREE,
                     bot.Keyboard.Keys.NUM_FOUR,
                     bot.Keyboard.Keys.NUM_FIVE,
                     bot.Keyboard.Keys.NUM_SIX,
                     bot.Keyboard.Keys.NUM_SEVEN,
                     bot.Keyboard.Keys.NUM_EIGHT,
                     bot.Keyboard.Keys.NUM_NINE,
                     bot.Keyboard.Keys.NUM_MULTIPLY,
                     bot.Keyboard.Keys.NUM_PLUS,
                     bot.Keyboard.Keys.NUM_MINUS,
                     bot.Keyboard.Keys.NUM_PERIOD,
                     bot.Keyboard.Keys.NUM_DIVISION],
                    '0123456789*+-./');
    }

    function testUnknownChar() {
      runTypingTest('\u7231');
    }

    function testBackspace() {
      // WEBKIT fires an input event but no textInput event on backspace.
      runTypingTest(['abcd', BACKSPACE], 'abc', 5, 4);
      runTypingTest(['abcd', LEFT, BACKSPACE, BACKSPACE, 'e'], 'aed', 7, 5);

      // GECKO browsers sometimes fire the input event on a backspace even if
      // the text was not changed.
      var inputs = goog.userAgent.GECKO ? 5 : 4;
      runTypingTest(['ab', LEFT, BACKSPACE, BACKSPACE, 'c'], 'cb', inputs, 3);
    }

    function testDelete() {
      // WEBKIT fires an input event but no textInput event on delete.
      runTypingTest(['abcd', LEFT, LEFT, DELETE, 'e'], 'abed', 6, 5);

      // Firefox 3.0 (which is version 1.9.0.*) always sends an input event
      // on delete in a textbox.
      if (goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.1')) {
        var inputIds = ['textbox', 'password'];
        runTypingTest(['abcd', DELETE, DELETE], 'abcd', 6, 4, inputIds);
        inputIds = ['textarea'];
        runTypingTest(['abcd', DELETE, DELETE], 'abcd', 4, 4, inputIds);
      } else {
        runTypingTest(['abcd', DELETE, DELETE], 'abcd', 4);
      }
    }

    function testEventCancel() {
      function cancelEvent(element, eventType, keyCode) {
        var listener = function(event) {
          // FF doesn't include keyCode on keypress events.
          var code = event.keyCode || event.charCode;
          if (code == keyCode) {
            event.preventDefault();
          }
        };
        return goog.events.listen(element, eventType, listener);
      }
      var listenerKeys = [];
      for (var i = 0; i < INPUT_IDS.length; i++) {
        var element = document.getElementById(INPUT_IDS[i]);
        listenerKeys.push(cancelEvent(element, goog.events.EventType.KEYDOWN,
            goog.events.KeyCodes.E));
        listenerKeys.push(cancelEvent(element, goog.events.EventType.KEYPRESS,
            'f'.charCodeAt(0)));
      }

      runTypingTest('abcdef', 'abcd');
      for (var i = 0; i < listenerKeys.length; i++) {
        goog.events.unlistenByKey(listenerKeys[i]);
      }
    }

    /**
     * Calls bot.action.type('textbox', input), and returns a listing of
     * what the modifier key state was for all the keypress events created.
     *
     * @return {{alt: Array.<boolean>, ctrl: Array.<boolean>,
     *           meta: Array.<boolean>, shift: Array.<boolean>}} What the value
     *   of particular modifier keys were for each of the keypress events
     *   generated by this command.
     */
    function runToggleTest(input) {
      /**
       * Creates a single listener on textbox that records what the value of a
       * modifier key was on a keypress.
       */
      function setupSingleToggleListener(history, key) {
        var listenerKey = (goog.events.listen(
            document.getElementById('textbox'),
            goog.events.EventType.KEYPRESS,
            function(event) {
              history[key].push(event[key + 'Key']);
            }));
        return listenerKey;
      }

      /**
       * Creates listeners on the page's textbox that record what the value of
       * each modifier key was on a keypress.
       *
       * @param {!Array.<number>} listenerKeys An array that will store the ids
       *   of each listener added to textbox by this function.  Needed to remove
       *   the listeners later.
       */
      function setupAllToggleListeners(listenerKeys) {
        var history = {alt: [], ctrl: [], meta: [], shift: []};
        for (key in history) {
          listenerKeys.push(setupSingleToggleListener(history, key));
        }
        return history;
      }

      var listenerKeys = [];
      var history = setupAllToggleListeners(listenerKeys);
      try {
        var textBox = document.getElementById('textbox');
        bot.action.type.apply(null, [textBox].concat(input));
      } finally {
        for (var i = 0; i < listenerKeys.length; i++) {
          goog.events.unlistenByKey(listenerKeys[i]);
        }
      }
      return history;
    }

    function testToggleKeys() {
      var history = runToggleTest(['ab', CONTROL, 'cd', SHIFT, 'ef']);
      assertTrue(goog.array.equals(
          [false, false, true, true, true, true], history['ctrl']));
      assertTrue(goog.array.equals(
          [false, false, false, false, true, true], history['shift']));

      history = runToggleTest(['ab', CONTROL, 'cd', CONTROL, 'ef']);
      assertTrue(goog.array.equals(
          [false, false, true, true, false, false], history['ctrl']));

      history = runToggleTest([ALT, 'a', SHIFT, 'b', ALT, 'c', SHIFT, 'd']);
      assertTrue(goog.array.equals(
          [true, true, false, false], history['alt']));
      assertTrue(goog.array.equals(
          [false, true, true, false], history['shift']));

      history = runToggleTest(['a', META, 'b']);

      // The meta key fires a keypress event in GECKO.
      if (goog.userAgent.GECKO) {
        assertTrue(goog.array.equals(
            [false, false, true], history['meta']));
      } else {
        assertTrue(goog.array.equals(
            [false, true], history['meta']));
      }
    }

    function testEnterNotTextArea() {
      // WEBKIT fires a textInput event but no input event on enter, see:
      // https://bugs.webkit.org/show_bug.cgi?id=54152
      var inputIds = ['textbox', 'password'];
      runTypingTest(['asdf', ENTER, 'qwer'], 'asdfqwer', 8, 9, inputIds);
    }

    function testEnterTextArea() {
      // Although this looks like a platform dependent change, these newlines
      // are what is seen. In particular, the Opera line ending is consistent
      // cross-platform.
      var newline = (bot.userAgent.IE_DOC_PRE9 || goog.userAgent.OPERA) ?
          '\r\n' : '\n';
      var finalText = 'asdf' + newline + 'qwer';
      runTypingTest(['asdf', ENTER, 'qwer'], finalText, 9, 9, ['textarea']);
    }

    function testTypingAppends() {
      runTypingTest('b', 'ab', 1, 1, INPUT_IDS, 'a');
    }

    function testCannotTypeInNotShownElement() {
      for (var i = 0; i < INPUT_IDS.length; i++) {
        var input = document.getElementById(INPUT_IDS[i]);
        input.style['visibility'] = 'hidden';
      }
      continueAfterDelayInIE7('visibility = hidden', function() {
        for (var i = 0; i < INPUT_IDS.length; i++) {
          var input = document.getElementById(INPUT_IDS[i]);
          assertThrows(goog.partial(bot.action.type, input, 'a'));
          input.style['visibility'] = 'visible';
        }
        continueAfterDelayInIE7('visibility = visible');
      });
    }

    function testCannotTypeInNotDisabledElement() {
      for (var i = 0; i < INPUT_IDS.length; i++) {
        var input = document.getElementById(INPUT_IDS[i]);
        input.disabled = true;
      }
      continueAfterDelayInIE7('disabled = true', function() {
        for (var i = 0; i < INPUT_IDS.length; i++) {
          var input = document.getElementById(INPUT_IDS[i]);
          assertThrows(goog.partial(bot.action.type, input, 'a'));
          input.disabled = false;
        }
        continueAfterDelayInIE7('disabled = false');
      });
    }

    function testTypingInReadOnlyInputDoesNotAddText() {
      for (var i = 0; i < INPUT_IDS.length; i++) {
        var input = document.getElementById(INPUT_IDS[i]);
        input.readOnly = true;
      }
      continueAfterDelayInIE7('readonly = true', function() {
        runTypingTest('text not added', '', 0, 0, ['textbox']);
        for (var i = 0; i < INPUT_IDS.length; i++) {
          var input = document.getElementById(INPUT_IDS[i]);
          input.readOnly = false;
        }
        continueAfterDelayInIE7('readonly = false');
      });
    }

    /**
     * In IE versions prior to 8, setting properties of an input element
     * causes some number of spurious, asynchronous propertychange events
     * on the 'value' property of the element, even though the 'value' property
     * is not the one being changed. These events interfere with our counting
     * the number of such events in runTypingTest. The precise number of
     * spurious events fired is non-deterministic, so we can't simply wait for
     * that number to complete. We resort to waiting a fixed delay after setting
     * the property of an input element on IE7.
     *
     * For IE versions prior to 8, this function resumes an async test case and
     * executes the optionally-provided function after a fixed delay. For all
     * other browsers, it just executes the function straightaway.
     */
    function continueAfterDelayInIE7(desc, opt_fn) {
      if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {
        asyncTestCase.waitForAsync('waiting for ' + desc);
        window.setTimeout(function() {
          asyncTestCase.continueTesting();
          if (opt_fn) {
            opt_fn();
          }
        }, 500);
      } else if (opt_fn) {
        opt_fn();
      }
    }
  </script>
</head>
<body>
  The last textbox displays how often some of the events of the first textbox
  fire. <br>
  <form action="" onsubmit="return false;">
    <input type="text" id="textbox"/>
    <input type="password" id="password"/>
    <input type="email" id="email"/>
    <input type="search" id="search"/>
    <textarea id="textarea"></textarea>
    <input type="text" id="inputCount"/>
    <input type="text" id="textInputCount"/>
  </form>
</body>
<script type="text/javascript">
  asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
</script>
</html>
